package ch.mailbox;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import ch.exception.MySQLErrorException;
import ch.exception.ObjectNotFoundException;


/** La classe astratta <b>PersistentObject</b> si occupa delle funzionalità MySQL
 * per la lettura, scrittura e aggiornamento degli oggetti delle classi derivate.
 * 
 * @author Neil Otupacca - May 21 2011
 *
 */
public abstract class PersistentObject {

	private Connection connection = null;

	// Attributi condivisi
	@Persistent
	@AutoDefaultReadOnly
	protected int id;
	@Persistent(name="email")
	protected String emailAddr;
	private String targetName;

	////////////////////////////////////
	// Dati di accesso al database MySQL
	private String myServerName = "localhost";
	private int myPort = 3306;
	private String myDatabase = "mailsystem";
	private String myUsername = "root";
	private String myPassword = "";
	////////////////////////////////////

	public PersistentObject(String targetName) {
		this.targetName = targetName;

		// Connessione MySQL
		try {
			// Carica il driver JDBC
			Class.forName("com.mysql.jdbc.Driver");

			// Apri la connessione al database (USE myDatabase implicito)
			String url = "jdbc:mysql://" + myServerName + ":" + myPort + "/" + myDatabase;
			connection = DriverManager.getConnection(url, myUsername, myPassword);
		} catch (ClassNotFoundException e) {
			// Database driver non trovato
			throw new MySQLErrorException(e.getMessage());
		} catch (SQLException e) {
			// Errore di connessione al database
			throw new MySQLErrorException(e.getMessage());
		}
	}


	/** Ritorna l'identificatore univoco dell'oggetto.
	 *
	 * @return identificatore univoco dell'oggetto.
	 */
	public int getId() {
		return(id);
	}

	/** Ritorna l'indirizzo email associato all'oggetto.
	 *
	 * @return indirizzo email.
	 */
	public String getEmailAddress() {
		return(emailAddr);
	}

	/** Ritorna una copia della stringa <b>query</b> dopo aver eseguito l'escaping
	 * dei caratteri <i>LF</i>, <i>CR</i>, <i>backslash</i>, <i>single-quote</i> e
	 * <i>double-quote</i>.
	 *
	 * @param query - unescaped string.
	 * @return escaped string.
	 */
	public static String escapeString(String query) {
		// Escape dei caratteri \n  \r  \  '  "
		query = query.replaceAll("\\\\", "\\\\\\\\");  // eseguire per primo
		query = query.replaceAll("\r", "\\\\r");
		query = query.replaceAll("\n", "\\\\n");
		query = query.replaceAll("'", "''");
		query = query.replaceAll("\"", "\\\\\"");
		return(query);
	}

	/** Ritorna una lista di String con i nomi dei methods dell'oggetto
	 * contrassegnati dalla <i>Annotation Type</i> <b>@Persistent</b>.
	 * Nella lista sono inclusi anche i methods della super classe.
	 * 
	 * @return methods name list.
	 */
	private List<Field> getFieldList() {
		List<Field> fieldList = new ArrayList<Field>();

		// Ottieni i fields dell'oggetto corrente (tramite reflection)
		Class<? extends PersistentObject> cls = this.getClass();
		Field fields[] = cls.getDeclaredFields();

		for ( int n = 0 ; n < fields.length ; n++ ) {
			// Annotation Type @Persistent del field
			Persistent ps = fields[n].getAnnotation(Persistent.class);
			if (ps != null) fieldList.add(fields[n]);
		}

		// Ottieni i fields della super classe dell'oggetto corrente
		fields = cls.getSuperclass().getDeclaredFields();

		for ( int n = 0 ; n < fields.length ; n++ ) {
			// Annotation Type @Persistent del field
			Persistent ps = fields[n].getAnnotation(Persistent.class);
			if (ps != null) fieldList.add(fields[n]);
		} 

		return(fieldList);
	}

	/** Aggiunge l'oggetto nel database.
	 *
	 * @return <b>id</b> generato dalla query INSERT.
	 */
	protected int add() {
		String query = "", values = "";
		Iterator<Field> fieldIterator = getFieldList().iterator();

		try {
			while(fieldIterator.hasNext()) {
				Field field = fieldIterator.next();

				// Non inserire i field di tipo Timestamp
				if (field.getAnnotation(AutoDefaultReadOnly.class) == null) {
					// Se la Annotation Type @Persistent non specifica un nome
					// di mapping, assumi il nome del field come colomnName
					String name = field.getAnnotation(Persistent.class).name();
					if (name.isEmpty()) name = field.getName();

					if (!query.isEmpty()) query += ",";
					query += name;

					if (!values.isEmpty()) values += ",";

					// Abilita l'accesso alla variabile nel caso sia privata
					field.setAccessible(true);
					String type = field.getType().getName();

					if (type == "int") values += field.getInt(this);
					else if (type == "boolean") values += field.getBoolean(this);
					else values += "'" + escapeString(field.get(this).toString()) + "'";
				}
			}

			query = "INSERT INTO " + targetName + " (" + query + ") VALUES (" + values + ")";

			// Esegui l'INSERT
			Statement stmt = connection.createStatement();
			stmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);

			ResultSet rs = stmt.getGeneratedKeys();

			// Determina l'id della row inserita (autoincrement)
			if (rs.next()) id = rs.getInt(1);
			else {
				throw new MySQLErrorException("No inserted ID");
			}

			rs.close();
			stmt.close();

			return(id);
		} catch (Exception e) {
			throw new MySQLErrorException(e.getMessage());
		}
	}

	/** Aggiorna l'oggetto nel database.
	 *
	 * @param id - identificatore univoco dell'oggetto.
	 * @return Numero di <i>rows</i> modificate dalla query UPDATE.
	 */
	protected int update() {
		String query = "";
		Iterator<Field> fieldIterator = getFieldList().iterator();

		try {
			while(fieldIterator.hasNext()) {
				Field field = fieldIterator.next();

				// Non inserire i field di tipo Timestamp
				if (field.getAnnotation(AutoDefaultReadOnly.class) == null) {
					// Se la Annotation Type @Persistent non specifica un nome
					// di mapping, assumi il nome del field come colomnName
					String name = field.getAnnotation(Persistent.class).name();
					if (name.isEmpty()) name = field.getName();

					if (!query.isEmpty()) query += ",";

					query += name + " = ";

					// Abilita l'accesso alla variabile nel caso sia privata
					field.setAccessible(true);
					String type = field.getType().getName();

					if (type == "int") query += field.getInt(this);
					else if (type == "boolean") query += field.getBoolean(this);
					else query += "'" + escapeString(field.get(this).toString()) + "'";
				}
			}

			query = "UPDATE " + targetName + " SET " + query + " WHERE id = " + id;

			// Esegui l'UPDATE
			Statement stmt = connection.createStatement();
			int rt = stmt.executeUpdate(query);
			stmt.close();

			return(rt);
		} catch (Exception e) {
			throw new MySQLErrorException(e.getMessage());
		}
	}

	/** Popola l'oggetto corrente con il risultato di una query.
	 * 
	 * @param rs - ResultSet della query.
	 */
	private void populateObject(ResultSet rs) {
		Iterator<Field> fieldIterator = getFieldList().iterator();

		try {
		// Itera i fields dell'oggetto
			while(fieldIterator.hasNext()) {
				Field field = fieldIterator.next();
	
				// Se la Annotation Type @Persistent non specifica un nome
				// di mapping, assumi il nome del field come colomnName
				String name = field.getAnnotation(Persistent.class).name();
				if (name.isEmpty()) name = field.getName();
	
				// Abilita l'accesso alla variabile nel caso sia privata
				field.setAccessible(true);
	
				// Setta il valore del field
				String type = field.getType().getName();
	
				if (type == "int") field.setInt(this, rs.getInt(name));
				else if (type == "boolean") field.setBoolean(this, rs.getBoolean(name));
				else field.set(this, rs.getString(name));
			}
		} catch (Exception e) {
			throw new MySQLErrorException(e.getMessage());
		}
	}

	/** Popola gli attributi dell'oggetto leggendoli dal database.
	 *
	 * @param filterById - identificatore stringa dell'oggetto.
	 */
	protected void getPersistentObject(boolean filterById) {
		try {
			Statement stmt = connection.createStatement();

			// Costruisci la query di SELECT
			String query = "SELECT * FROM " + targetName + " WHERE email = '" +
						   escapeString(emailAddr) + "'";
			if (filterById) query += " AND id = " + id;

			ResultSet rs = stmt.executeQuery(query);

			if (rs != null) {
				if (!rs.next()) {
					rs.close();
					stmt.close();
					throw new ObjectNotFoundException();
				}
				else {
					// Popola l'oggetto corrente con il risultato della query
					populateObject(rs);
					rs.close();
					stmt.close();
				}
			}
		} catch (SQLException e) {
			throw new MySQLErrorException(e.getMessage());
		} catch (IllegalArgumentException e) {
			throw new MySQLErrorException(e.getMessage());
		}
	}

	/** Elimina l'oggetto dal database.
	 *
	 * @return numero di oggetti cancellati dal database (0 - 1).
	 */
	public int delete() {
		String query = "DELETE FROM " + targetName + " WHERE id = " + id + " LIMIT 1";
		int rt = 0;

		try {
			Statement stmt = connection.createStatement();
			rt = stmt.executeUpdate(query);
			stmt.close();
		} catch (SQLException e) {
			throw new MySQLErrorException(e.getMessage());
		}

		return(rt);
	}

	/** Ritorna un'array di oggetti User o Message popolati da una query.
	 *
	 * @param filterByOwner - boolean per il filtering con la clausola WHERE.
	 * @param sort - stringa per la clausola ORDER BY.
	 * @return Array di collection della lista di oggetti.
	 */
	protected ArrayList<? extends PersistentObject> getPersistentObjectList(boolean filterByOwner,
																		 	String sort) {
		try {
			// Costruisci la query SQL
			Statement stmt = connection.createStatement();
			String query = "SELECT * FROM " + targetName;

			// Aggiungi la clausola WHERE se flFilter è true
			if (filterByOwner) query += " WHERE email = '" + escapeString(emailAddr) + "'";

			// Aggiungi la clausola ORDER BY se sort non è empty
			if (!sort.isEmpty()) query += " ORDER BY " + sort;

			ResultSet rs = stmt.executeQuery(query);

			//ArrayList<HashMap<String, String>> rowArray = new ArrayList<HashMap<String, String>>();
			ArrayList<PersistentObject> objArray = new ArrayList<PersistentObject>();

			if (rs != null) {
				while (rs.next()) {
					// Crea un oggetto del tipo di this
					PersistentObject obj =  this.getClass().newInstance();
					obj.populateObject(rs);

					// Aggiungi l'oggetto
					objArray.add(obj);
				}

				rs.close();
			}

			stmt.close();

			return(objArray);
		} catch (Exception e) {
			throw new MySQLErrorException(e.getMessage());
		}
	}
}
